《Effective Java》第九章:异常
充分发挥异常的优点,可以提高程序的可读性、可靠性和可维护性。如果使用不当,它们也会带来负面影响。
第57条:只针对异常的情况下使用异常
先看下如下代码:1
2
3
4
5
6
7
8
9try
{
int i=0;
while(true)
range[i++].climd();
}catch(ArrayIndexOutOfBoundsException e)
{
}
此代码看的如此蛋疼,使用异常来跳过数组越界,其实完全可以用下面的代码代替1
2for(Mountain m:range)
m.climb();
简单明了(我想上面的例子大家都能看出孰优孰劣)
不推荐第一种写法的原因是:
- 因为异常机制的设计初衷是用于不正常情形,所以很少会有
JVM
实现试图对它们进行优化,使得与显示的测试一样快速 - 把代码放在
try-catch
块中反而阻止了现代JVM
实现本来可能要执行的某些特征优化 - 对数组进行遍历的标准模式并不会导致冗余检查。有些现代的
JVM
实现会将它们优化掉
所以,异常应该只用于异常的情况下,它们永远不应该用于正常的控制流
第58条:对可恢复的情况使用首检异常,对编程错误使用运行时异常
Java提供了三种可抛出的结构:
- 受检的异常(checked exception)—
Exception
的子类 - 运行时异常(run-time exception)—
RuntimeException
的子类 - 错误(error)—
Error
的子类
如果期望调用者能够适当得恢复,对于这种情况应该使用受检的异常。通过抛出受检的异常,强迫调用者在一个catch
子句中处理该异常,或者将其传播出去。
对于非受检的异常,往往就是属于不可恢复的情形,继续执行下去有害无益。
- 大多数运行时异常都表示前提违例,所谓的前提违例是指没有遵循API中规范建立的约定。例如数组访问的约定指明了下标值必须在零到数组长度之间,不然就会抛
ArrayIndexOutBoundsException
。 - 按照惯例,错误往往是表示资源不足、约束失败,或者其他程序无法继续执行的条件。
总而言之,对于可恢复的情况,使用受检的异常,对于程序错误,使用运行时异常。
第59条:避免不必须要使用受检的异常
我只想对作者说,wtf,本条都被你翻译得看不懂了-_-
受检的异常虽好,但是千万别滥用,由于每抛出一个受检的异常,使用者都需要去catch
它或者再次抛出去,所以如果能避免这种异常则应该尽量避免,这个时候你可以考虑重构。
比如1
2
3
4
5
6
7try
{
obj.action(args);
}catch(TheCheckedException e)
{
//处理异常
}
可以重构为1
2
3
4
5
6if(obj.actionpermitted(args))//进行相关处理检查
{
obj.action(args)
}else{
//处理异常
}
当然这个重构只是一个例子,这种重构也并不是万能的。
第60条:优先使用标准的异常
本条的意思是就算你再牛,也还是建议使用标准的异常而不是自定义的异常,除非标准的异常真的无法满足你的需求,因为:
- 标准的异常已经为广大程序猿熟知
- 可以使你的API更加易于学习和使用
- 异常类越少,装在这些类的时间开销也越小(这条是不是有点牵强了,一个大项目中应该不会因为几个自定义的异常类而导致性能差吧-_-)
下面对几个最常使用的异常:
IllegalAargumentException
:非null
参数值不正确IllegalStateException
:对于方法调用而言,对象状态不合适NullPointException
:在禁止使用null
的情况下参数值为null
IndexOutOfBoundsException
:下标参数值越界ConcurrentModificationException
:在禁止冰法修改的情况下,检测到对象的并发修改(在迭代器里面修改了原始数据的值就会抛)UnsupportedOperationException
:对象不支持用户请求的方法
这里在使用具体异常中并有精确的标准,只是在选取的时候选一个最合适的^_^
第61条:抛出与抽象相对应的异常
在调用高层的API时不应该抛出低层的异常,否则会使人不知所措,同时会暴露低层里面的相关实现细节,这个时候就需要异常转译,比如以AbstractSequentialList
实现了List
这个接口,它里面的一个get
方法就进行了异常转译:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* Returns the element at the specified position in this list.
*
* <p>This implementation first gets a list iterator pointing to the
* indexed element (with <tt>listIterator(index)</tt>). Then, it gets
* the element using <tt>ListIterator.next</tt> and returns it.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);//这里抛出了更加高层的异常
}
}
当然时候是希望能看到低层的异常,这样可以用于调试,那么你可以使用异常链:1
2
3
4
5
6
7
8try
{
//...to something
}catch(LowerLevelException cause)
{
//这里抛出高层的异常中会带有低层异常的信息
throw new HighLevelException(cause)
}
异常转译和异常链也不能被滥用,如果可能,处理来自低层异常的最好做法就是在调用低层方法之前确保它们会执行成功,从而避免它们抛出异常,比如在执行低层方法之前先对需要传递参数的检查就是一个好的方法。
第62条:每个方法抛出的异常都要有文档
这条的名称就是最好的建议^_^
- 使用要单独的声明受检的异常,并且利用JavaDoc的@throw标记,准确的记录下抛出异常的每个条件(个人感觉非常重要,用这种文档的API就会非常的舒心)
- 使用JavaDoc的@throws标签记录下一个方法可能抛出的一个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中(注意@throws标签和throws关键字的区别)
- 如果一个类中许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,而不是在每个方法上都去标注(这条不错^_^)
第63条:在细节消息中包含能捕获失败的信息
异常数据中最有用的是那些导致异常的硬数据,以IndexOutOfBoundsException
为例,该异常抛出时就应该有正确的上界,下界以及没有落入界内的下标值,这样就可以非常轻松的让开发人员了解异常出现的原因以及如何去fix它.
为了确保异常在细节描述中包含足够能捕获失败的信息,最好的方法是在异常的构造器而不是字符串细节中引用这些消息(因为这些字符串细节其实并没有什么卵用),比如IndexOutOfBoundsException
就包含了具体参数的构造函数1
2
3
4
5
6
7
8
9
10public IndexOutOfBoundsException(int lowerBound,int upperBound,int index)
{
super("Lower Bound:"+lowerBound+
",Upper bound:"+upperBound+
",Index:"+index);
this.lowerBound=lowerBound;
this.upperBound=upperBound;
this.index=index;
}
这种构造函数相对于接收字符串类型的构造函数来说更加易于描述异常出现的情况。
但是我看了JDK源码,
IndexOutOfBoundsException
并没有这个构造函数啊-_-
第64条:努力使失败保持原子性
当对象抛出异常之后,通常我们期望这个对象仍然保持在一个定义良好的可用状态之中。一般而言,失败的而方法调用应该是对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性。
如果对象时不可变的,那么在方法失败之后该原子性一定还是保持的。
如果对象时可变的,就有如下四种方法可以保持原子性:
- 在执行操作之前检查参数的有效性
- 调整计算处理过程中的顺序,是的任何可能会失败的计算部分在对象被修改之前发生
- 编写一段回复代码,有它来拦截操作过程中发生的失败,以及使对象回到到操作开始之前的状态上(较为麻烦,但可能是最有效的)
- 在对象的一份临时拷贝上执行操作,操作完成之后再用临时拷贝代替对象的内容(Collections.sort就是这么干的)
1 | public static <T extends Comparable<? super T>> void sort(List<T> list) { |
作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态。
第65条:不要忽略异常
好比将火警的信号器关掉了,当真正发生火灾的时候,就没有人能看到火警信号了,这结果也许是灾难性的。(形象^_^)
如果一定要忽略异常,你至少要在catch
块中解释为什么可以忽略。
还有在关闭FileInputStream
的时候,你可以忽略它的异常,因为你还没有改变文件的状态,因此不必执行任何回复动作。